package org.de.maeyer.statistic.view.ajax;

import static java.lang.Math.*;
import static org.de.maeyer.unit.UnitUtils.*;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

import org.de.maeyer.sampler.ConcurrentSamplerManager;
import org.de.maeyer.sampler.DoubleSampler;
import org.de.maeyer.sampler.LongSampler;
import org.de.maeyer.sampler.ObjectSampler;
import org.de.maeyer.sampler.SamplerManager;
import org.de.maeyer.statistic.Category;
import org.de.maeyer.statistic.Statistics;
import org.de.maeyer.unit.NamedUnit;
import org.de.maeyer.unit.Unit;

/**
 * @author Peter De Maeyer
 */
public class AjaxChartBuilder {
	
	private final List<Statistics> allStatistics = new LinkedList<Statistics>();
	
	private final List<Statistics> columnCharts = new LinkedList<Statistics>();
	
	private final List<Statistics> scatterCharts = new LinkedList<Statistics>();
	
	private final List<Statistics> gauges = new LinkedList<Statistics>();
	
	public void append(Statistics statistics) {
		allStatistics.add(statistics);
		if (statistics.hasHistogram()) {
			if (statistics.getValueClass().equals(double.class)) {
				scatterCharts.add(statistics);
			} else {
				columnCharts.add(statistics);
			}
		}
		if (statistics.hasLastValue() && statistics.hasExtrema() && statistics.getValueClass().equals(long.class)) {
			gauges.add(statistics);
		}
	}
	
	public void toAjaxChart(OutputStream out) {
		PrintWriter writer = new PrintWriter(out);
		writer.println("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
		writer.println("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
		writer.println("  <head>");
		writer.println("    <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"></meta>");
		writer.println("    <title>");
		writer.println("      Statistics");
		writer.println("    </title>");
		writer.println("    <script type=\"text/javascript\" src=\"http://www.google.com/jsapi\"></script>");
		
		// Write script to load the correct packages.
	    writer.println("    <script type=\"text/javascript\">");
	    StringBuilder packages = new StringBuilder(200).append("      google.load('visualization', '1', {packages: [");
	    if (!columnCharts.isEmpty()) {
	    	packages.append("'columnchart', 'piechart', ");
	    }
	    if (!scatterCharts.isEmpty()) {
	    	packages.append("'scatterchart', ");
	    }
	    if (!gauges.isEmpty()) {
	    	packages.append("'gauge', ");
	    }
    	if (hasTrailingComma(packages)) {
    		packages.replace(packages.length() - 2, packages.length(), "]});");
    	} else {
    		packages.append("]});");
    	}
    	writer.println(packages.toString());
	    writer.println("    </script>");
	    
	    // Write scripts to draw the charts.
	    writer.println("    <script type=\"text/javascript\">");
		
		int columnChart = 0;
		int scatterChart = 0;
		int gauge = 0;
		for (Statistics statistics: allStatistics) {
			if (columnChart < columnCharts.size() && statistics == columnCharts.get(columnChart)) {
				List<Category> categories = statistics.getCategories();
		    	writer.println("      function drawColumnChart" + columnChart + "() {");
		    	writer.println("        var data = new google.visualization.DataTable();");
		    	writer.println("        data.addColumn('string', 'Category');");
		    	writer.println("        data.addColumn('number', 'Count');");
		    	writer.println("        data.addRows(" + categories.size() + ");");
		    	int row = 0;
		    	for (Category category: categories) {
		    		writer.println("        data.setCell(" + row + ", 0, '" + category.getValue() + "');");
		    		writer.println("        data.setCell(" + row + ", 1, " + category.getCount() + ");");
		    		row++;
		    	}
		    	writer.println("        new google.visualization.ColumnChart(document.getElementById('columnchart" + columnChart + "')).draw(data, null);");
		    	writer.println("        new google.visualization.PieChart(document.getElementById('piechart" + columnChart + "')).draw(data, null);");
		    	writer.println("      }");
		    	writer.println("      google.setOnLoadCallback(drawColumnChart" + columnChart + ");");
		    	columnChart++;
			}
			if (scatterChart < scatterCharts.size() && statistics == scatterCharts.get(scatterChart)) {
				List<Category> categories = statistics.getCategories();
		    	writer.println("      function drawScatterChart" + scatterChart + "() {");
		    	writer.println("        var data = new google.visualization.DataTable();");
		    	writer.println("        data.addColumn('number', 'Value');");
		    	writer.println("        data.addColumn('number', 'Density');");
		    	writer.println("        data.addRows(" + (categories.size() - 1) * 2 + ");");
		    	int row = 0;
	    		writer.println("        data.setCell(" + row + ", 0, " + categories.get(1).getValue() + ");");
	    		writer.println("        data.setCell(" + row++ + ", 1, 0);");
		    	for (int i = 1, n = categories.size() - 1; i < n; i++) {
		    		Category category = categories.get(i);
		    		Category nextCategory = categories.get(i + 1);
		    		writer.println("        data.setCell(" + row + ", 0, " + category.getValue() + ");");
		    		writer.println("        data.setCell(" + row++ + ", 1, " + category.getDensity() + ");");
		    		writer.println("        data.setCell(" + row + ", 0, " + nextCategory.getValue() + ");");
		    		writer.println("        data.setCell(" + row++ + ", 1, " + category.getDensity() + ");");
		    	}
	    		writer.println("        data.setCell(" + row + ", 0, " + categories.get(categories.size() - 1).getValue() + ");");
	    		writer.println("        data.setCell(" + row++ + ", 1, 0);");
	    		writer.println("        var options = {lineSize: 2, pointSize: 0};");
		    	writer.println("        new google.visualization.ScatterChart(document.getElementById('scatterchart" + scatterChart + "')).draw(data, options);");
		    	writer.println("      }");
		    	writer.println("      google.setOnLoadCallback(drawScatterChart" + scatterChart + ");");
		    	scatterChart++;
			}
			if (gauge < gauges.size() && statistics == gauges.get(gauge)) {
		    	long lastValue = ((Number) statistics.getLastValue()).longValue();
		    	writer.println("      function drawGauge" + gauge + "() {");
		    	writer.println("        var data = new google.visualization.DataTable();");
		    	writer.println("        data.addColumn('string', 'Unit');");
		    	writer.println("        data.addColumn('number', 'Value');");
		    	writer.println("        data.addRows(1);");
		    	writer.println("        data.setCell(0, 0, '" + statistics.getUnit().getSymbol() + "');");
		    	writer.println("        data.setCell(0, 1, " + lastValue + ");");
		    	StringBuilder options = new StringBuilder(200).append("        var options = {");
		    	long minimum = -1;
		    	long maximum = -1;
		    	if (statistics.hasExtrema()) {
		    		minimum = ((Number) statistics.getMinimum()).longValue();
		    		maximum = ((Number) statistics.getMaximum()).longValue();
					options.append("min: ").append(minimum).append(", ");
					options.append("max: ").append(maximum).append(", ");
		    	}
		    	if (statistics.hasMean() && statistics.hasVariance()) {
		    		double mean = statistics.getMean();
		    		double stdDev = statistics.getSampleStdDev();
		    		int marker1 = (int) round(mean - stdDev);
		    		int marker2 = (int) round(mean + stdDev);
		    		options.append("greenFrom: ").append(minimum != -1 ? minimum : 0).append(", greenTo: ").append(marker1).append(", ");
		    		options.append("yellowFrom: ").append(marker1).append(", yellowTo: ").append(marker2).append(", ");
		    		options.append("redFrom: ").append(marker2).append(", redTo: ").append(maximum != -1 ? maximum : 100).append(", ");
		    	}
		    	if (hasTrailingComma(options)) {
		    		options.replace(options.length() - 2, options.length(), "};");
		    	} else {
		    		options.append("};");
		    	}
		    	writer.println(options.toString());
		    	writer.println("        new google.visualization.Gauge(document.getElementById('gauge" + gauge + "')).draw(data, options);");
		    	writer.println("      }");
		    	writer.println("      google.setOnLoadCallback(drawGauge" + gauge + ");");
		    	gauge++;
			}
		}
	    writer.println("    </script>");
		writer.println("  <head>");
		writer.println("  <body style=\"font-family: Arial;border: 0 none;\">");
		columnChart = 0;
		scatterChart = 0;
		gauge = 0;
		for (Statistics statistics: allStatistics) {
			Unit unit = statistics.getUnit();
			writer.println("    <h1>" + statistics.getName() + (unit != null ? " (" + unit + ")" : "") + "</h1>");
			writer.println("    <ul>");
			if (statistics.hasLastValue()) {
				writer.println("      <li>last value: " + statistics.getLastValue() + "</li>");
			}
			if (statistics.hasCount()) {
				writer.println("      <li>count: " + statistics.getCount() + "</li>");
			}
			if (statistics.hasMean()) {
				writer.println("      <li>mean: " + statistics.getMean() + "</li>");
			}
			if (statistics.hasVariance()) {
				writer.println("      <li>sample standard deviation: " + statistics.getSampleStdDev() + "</li>");
				writer.println("      <li>population standard deviation: " + statistics.getPopulationStdDev() + "</li>");
			}
			if (statistics.hasElapsedTime()) {
				writer.println("      <li>elapsed time: " + statistics.getElapsedTime(SECOND) + " " + SECOND + "</li>");
			}
			if (statistics.hasRate()) {
				writer.println("      <li>rate: " + statistics.getRate(SECOND) + " /" + SECOND + "</li>");
			}
			if (statistics.hasTotal()) {
				writer.println("      <li>total: " + statistics.getTotal() + "</li>");
			}
			if (statistics.hasThroughput()) {
				writer.println("      <li>throughput: " + statistics.getThroughput(statistics.getUnit(), SECOND) + " " + statistics.getUnit() + "/" + SECOND + "</li>");
			}
			if (statistics.hasHistogram()) {
				List<Category> categories = statistics.getCategories();
				for (int modeCategoryIndex: statistics.getModeCategoryIndexes()) {
					writer.println("      <li>mode: " + categories.get(modeCategoryIndex).getValue() + "</li>");
				}
			}
			writer.println("    </ul>");
			if (columnChart < columnCharts.size() && statistics == columnCharts.get(columnChart)) {
				writer.println("    <table border='0' cellpadding='0' cellspacing='0'>");
				writer.println("      <tr>");
				writer.println("        <td><div id=\"columnchart" + columnChart + "\" style=\"width: 640px; height: 320px;\"></div></td>");
				writer.println("        <td><div id=\"piechart" + columnChart + "\" style=\"width: 320px; height: 320px;\"></div></td>");
				writer.println("      </tr>");
				writer.println("    </table>");
//				writer.println("    <div id=\"columnchart" + histogram + "\" style=\"width: 640px; height: 320px;\"></div>");
//				writer.println("    <div id=\"piechart" + histogram + "\" style=\"width: 320px; height: 320px;\"></div>");
				columnChart++;
			}
			if (scatterChart < scatterCharts.size() && statistics == scatterCharts.get(scatterChart)) {
				writer.println("    <div id=\"scatterchart" + scatterChart + "\" style=\"width: 640px; height: 320px;\"></div>");
				scatterChart++;
			}
			if (gauge < gauges.size() && statistics == gauges.get(gauge)) {
				writer.println("    <div id=\"gauge" + gauge + "\" style=\"width: 160px; height: 160px;\"></div>");
				gauge++;
			}
		}
	    writer.println("  </body>");
		writer.println("</html>");
		writer.flush();
	}
	
	private static final boolean hasTrailingComma(StringBuilder builder) {
		return builder.charAt(builder.length() - 2) == ',';
	}
	
	public static void main(String[] args) throws IOException {
	    SamplerManager samplerManager = new ConcurrentSamplerManager();
	    ObjectSampler carSampler = samplerManager.newObjectSampler("Car", null).accumulateHistogram();
	    sample(carSampler, "BMW", 35);
	    sample(carSampler, "Audi", 15);
	    sample(carSampler, "Peugeot", 55);
	    sample(carSampler, "Maserati", 4);
	    sample(carSampler, "Rolls Royce", 19);
	    LongSampler cpuSampler = samplerManager.newLongSampler("CPU", new NamedUnit("bogomips", "bm")).accumulateLastValue().accumulateExtrema();
	    cpuSampler.sample(0);
	    cpuSampler.sample(100);
	    cpuSampler.sample(75);
	    LongSampler memorySampler = samplerManager.newLongSampler("Memory", mega(BYTE)).accumulateLastValue().accumulateExtrema().accumulateVariance();
	    memorySampler.sample(0);
	    memorySampler.sample(100);
	    sample(memorySampler, 67, 50);
	    memorySampler.sample(75);
	    DoubleSampler energySampler = samplerManager.newDoubleSampler("Energy", JOULE).accumulateHistogram(-1., -.7, -.4, -.2, -.15, -.1, -.05, 0., .05, .1, .15, .2, .4, .7, 1.);
	    Random random = new Random();
	    for (int i = 0; i != 100000; i++) {
	    	energySampler.sample(random.nextGaussian() / 5.);
	    }
	    LongSampler overflowSampler = samplerManager.newLongSampler("Overflow", KELVIN).accumulateExtrema().accumulateLastValue().accumulateVariance();
	    sample(overflowSampler, 0L, 1000);
	    sample(overflowSampler, 100L, 1000);
	    sample(overflowSampler, 10L, 1);

	    AjaxChartBuilder chartBuilder = new AjaxChartBuilder();
	    chartBuilder.append(cpuSampler.getStatistics());
	    chartBuilder.append(carSampler.getStatistics());
	    chartBuilder.append(memorySampler.getStatistics());
	    chartBuilder.append(energySampler.getStatistics());
	    chartBuilder.append(overflowSampler.getStatistics());
	    File file = new File("/tmp/statistics.html");
	    FileOutputStream out = new FileOutputStream(file);
	    try {
	    	chartBuilder.toAjaxChart(out);
	    } finally {
	    	out.close();
	    }
	    System.out.println("Written statistics to: " + file);
    }
	
	private static void sample(ObjectSampler sampler, Object value, long count) {
		for (long i = 0L; i != count; i++) {
			sampler.sample(value);
		}
	}
	
	private static void sample(LongSampler sampler, long value, long count) {
		for (long i = 0L; i != count; i++) {
			sampler.sample(value);
		}
	}
}
